Skip to content

Rebuild graph update system with contract-driven relationship semantics v2 rebuild: EF Core 10 targeting with modular refactoring#13

Closed
WahidBitar wants to merge 15 commits into
masterfrom
001-rebuild-graph-update
Closed

Rebuild graph update system with contract-driven relationship semantics v2 rebuild: EF Core 10 targeting with modular refactoring#13
WahidBitar wants to merge 15 commits into
masterfrom
001-rebuild-graph-update

Conversation

@WahidBitar
Copy link
Copy Markdown
Owner

@WahidBitar WahidBitar commented Apr 5, 2026

Summary by CodeRabbit

  • New Features

    • EF Core 10 support and a rebuilt v2 graph-update flow with richer validation and explicit error types for unsupported or unloaded navigation mutations.
  • Tests

    • Large expansion of unit and integration tests covering many-to-many, one-to-one, payload associations, and rejection/validation scenarios.
  • Documentation

    • README updated with v2 usage example, supported relationship patterns, and rejection semantics.
  • Chores

    • Modernized .NET 10 build/publish workflow and restructured test/integration projects.

Update actions/checkout from v2 to v4, actions/setup-dotnet from v1 to v4, and change dotnet-version to "10.0.x". Also fix indentation and remove extra spaces in branch names.
Add a new publish job to the CI pipeline that builds, packs, and publishes the NuGet package on master pushes, with conditional checks for API key availability. Update action versions (checkout@v6, setup-dotnet@v5), adjust commands to target the solution file, and refine job names and permissions.

BREAKING CHANGE: package version bumped from 2.0.0 to 10.0.0 to align with EF Core 10+ targeting, which may introduce incompatibilities.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Refactors graph-update into a GraphUpdateOrchestrator with strategy modules, adds typed exceptions, migrates the library and tests to .NET 10 / EF Core 10, replaces legacy fake model/tests with a new TestModel and containerized integration test suite, and introduces many new unit and integration tests.

Changes

Cohort / File(s) Summary
CI & repo config
\.github/workflows/dotnet-core.yml, \.gitignore
CI workflow renamed/split, updated to .NET SDK 10, added publish gating and permissions; .gitignore now ignores JetBrains files.
Solution & projects
src/EFCore.UpdateGraph.slnx, EfCore.UpdateGraph.sln, src/Diwink.Extensions.EntityFrameworkCore/Diwink.Extensions.EntityFrameworkCore.csproj
Added new .slnx, removed old .sln, migrated library to net10.0, pinned EF Core 10.0.5, added packaging metadata and InternalsVisibleTo for tests.
Removed legacy model/tests
src/FakeModel/*, src/UnitTests/*
Deleted legacy FakeModel, NUnit unit tests, migrations, DbContext factory, appsettings and old unit test project.
New TestModel project
src/Diwink.Extensions.EntityFrameworkCore.TestModel/*
Added TestModel project targeting net10.0 with entities (Course, LearningCatalog, Mentor, TopicTag, CoursePolicy, MentorWorkspace, CourseMentorAssignment), configurations, and TestDbContext.
Integration test infra
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/...
Added integration test project, SqlServerContainerFixture (Testcontainers), DatabaseBootstrap (retry/reset), IntegrationTestBase/collection, SeedData, and many integration contract tests.
Unit test projects & suites
src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/...
Added unit test project and extensive tests covering EntityKeyHelper, GraphUpdate exceptions, classification, strategies, operation guard, and relationship semantics.
Core orchestration & API
src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs, src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/*, src/Diwink.Extensions.EntityFrameworkCore/Traversal/*
Replaced inline recursive diff with GraphUpdateOrchestrator.UpdateGraph and modular components: EntityKeyHelper, GraphUpdateOrchestrator, OperationGuard, NavigationLoadGuard, RelatedEntityMutationService.
Relationship strategies
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/*
Added strategies: PureManyToManyStrategy, PayloadManyToManyStrategy, RequiredOneToOneStrategy, OptionalOneToOneStrategy, and OneToOneOwnershipResolver.
Exceptions
src/Diwink.Extensions.EntityFrameworkCore/Exceptions/*
Introduced GraphUpdateException base and concrete exceptions: UnloadedNavigationMutationException, UnsupportedNavigationMutatedException, PartialMutationNotAllowedException, AmbiguousOwnershipSemanticsException, UnsupportedRelationshipPatternException.
Docs
README.md
Updated README for v2 rebuild (EF Core 10+), documented supported patterns, rejection semantics, and usage example.
Test project files
src/Diwink.Extensions.EntityFrameworkCore.Tests.{Integration,Unit}/*.csproj
Added new test project csprojs targeting net10.0 with appropriate test packages and project references.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller as Caller
    participant Orch as GraphUpdateOrchestrator
    participant Guard as OperationGuard
    participant Loader as NavigationLoadGuard
    participant Strategy as RelationshipStrategy
    participant Db as DbContext

    Caller->>Orch: UpdateGraph(context, updated, existing)
    activate Orch
    Orch->>Loader: EnsureNavigationsLoaded(existing)
    Loader-->>Orch: loaded/unloaded info
    Orch->>Guard: ValidateNavigations(updated, existing)
    Guard-->>Orch: validation result
    alt validation failed
        Orch-->>Caller: throw GraphUpdateException / PartialMutationNotAllowedException
    else validation passed
        Orch->>Db: Update scalar properties on existing
        Orch->>Strategy: For each loaded navigation: Classify -> Apply changes
        Strategy->>Db: add/remove/update tracked entities & links
        Strategy-->>Orch: applied
        Orch-->>Caller: return tracked existing entity
    end
    deactivate Orch
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through graphs both new and old,

Split strategies tidy, exceptions bold.
Keys and navigations matched with care,
Tests in containers running everywhere.
V2 springs forward — nimble, tested, fair.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 001-rebuild-graph-update

@coderabbitai coderabbitai Bot changed the title Rebuild graph update system with contract-driven relationship semantics @coderabbit Rebuild graph update system with contract-driven relationship semantics v2 rebuild: EF Core 10 targeting with modular refactoring Apr 5, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (16)
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj (1)

12-15: Pin floating package versions to keep CI restores deterministic.

Lines 12, 13, and 15 use wildcard versions (8.*, 10.*, 4.*), which can resolve differently over time and make integration test behavior drift between runs.

Proposed change
-    <PackageReference Include="FluentAssertions" Version="8.*" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.*" />
+    <PackageReference Include="FluentAssertions" Version="8.9.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
-    <PackageReference Include="Testcontainers.MsSql" Version="4.*" />
+    <PackageReference Include="Testcontainers.MsSql" Version="4.0.0" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj`
around lines 12 - 15, Replace the floating wildcard package versions with exact,
pinned versions to ensure deterministic CI restores: update the PackageReference
entries for FluentAssertions, Microsoft.EntityFrameworkCore.SqlServer, and
Testcontainers.MsSql (the elements with Include="FluentAssertions",
Include="Microsoft.EntityFrameworkCore.SqlServer",
Include="Testcontainers.MsSql") to use explicit version numbers instead of
"8.*", "10.*", and "4.*" respectively (leave Microsoft.NET.Test.Sdk as-is if
already pinned); commit the csproj change so CI always restores the specified
exact package versions.
src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs (1)

12-23: Validate relationshipType before storing it.

Line 22 sets a contract property from unvalidated input; enforce non-empty values to keep error payloads dependable.

Proposed fix
     public UnsupportedNavigationMutatedException(
         string relationshipPath,
         string relationshipType)
         : base(
             $"Mutation detected in unsupported navigation '{relationshipPath}' " +
             $"(relationship type: {relationshipType}). This relationship type is not " +
             "in the v2 supported contract. The entire operation was rejected. " +
             "Unchanged unsupported navigations would have been silently skipped.",
             relationshipPath)
     {
+        ArgumentException.ThrowIfNullOrWhiteSpace(relationshipType);
         RelationshipType = relationshipType;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs`
around lines 12 - 23, The UnsupportedNavigationMutatedException constructor
stores the unvalidated parameter relationshipType into the RelationshipType
property; validate that relationshipType is not null/empty (e.g., throw
ArgumentNullException or ArgumentException after trimming) before assigning it.
Update the UnsupportedNavigationMutatedException(string relationshipPath, string
relationshipType) constructor to check relationshipType (and relationshipPath if
desired) and throw an appropriate exception for invalid values, then assign
RelationshipType only after validation so error payloads remain dependable.
src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs (1)

16-16: Return an immutable view for Errors.

Line 16 exposes the backing List<GraphUpdateException> instance. Internal callers can cast back to List<T> and mutate state outside AddError.

Proposed fix
-    public IReadOnlyList<GraphUpdateException> Errors => _errors;
+    public IReadOnlyList<GraphUpdateException> Errors => _errors.AsReadOnly();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs` at
line 16, The Errors property currently returns the mutable backing _errors list
directly; change the getter to return an immutable view (e.g. wrap _errors with
ReadOnlyCollection via _errors.AsReadOnly() or return a ReadOnlyCollection
cached field) so callers cannot cast back to List<T> and mutate state; keep
mutation confined to AddError and reference GraphUpdateException and
_errors/Errors/AddError to locate the change.
src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs (1)

11-20: Validate missingDetail to preserve exception quality.

MissingDetail is a public diagnostic field; Lines 11–20 should reject null/whitespace input.

Proposed fix
     public AmbiguousOwnershipSemanticsException(
         string relationshipPath,
         string missingDetail)
         : base(
             $"Ambiguous ownership semantics at '{relationshipPath}': {missingDetail}. " +
             "The contract requires explicit requiredness/ownership metadata.",
             relationshipPath)
     {
+        ArgumentException.ThrowIfNullOrWhiteSpace(missingDetail);
         MissingDetail = missingDetail;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs`
around lines 11 - 20, The constructor AmbiguousOwnershipSemanticsException
should validate the missingDetail parameter and reject null/whitespace to
preserve exception integrity; inside the
AmbiguousOwnershipSemanticsException(string relationshipPath, string
missingDetail) constructor, check string.IsNullOrWhiteSpace(missingDetail) and
throw an ArgumentException or ArgumentNullException with a clear message
(including the parameter name "missingDetail") before calling base, ensuring the
public MissingDetail property is only assigned valid non-empty text.
src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs (1)

11-20: Enforce non-empty unsupportedBranch input.

Line 19 persists unsupportedBranch directly; null/blank values degrade the aggregated rejection message.

Proposed fix
     public PartialMutationNotAllowedException(
         string relationshipPath,
         string unsupportedBranch)
         : base(
             $"Graph operation rejected: unsupported mutation detected at '{unsupportedBranch}'. " +
             "The entire operation was rejected — partial mutation is not allowed.",
             relationshipPath)
     {
+        ArgumentException.ThrowIfNullOrWhiteSpace(unsupportedBranch);
         UnsupportedBranch = unsupportedBranch;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs`
around lines 11 - 20, The constructor PartialMutationNotAllowedException should
validate that the incoming unsupportedBranch is not null/empty/whitespace before
assigning; update the PartialMutationNotAllowedException(string
relationshipPath, string unsupportedBranch) constructor to check
string.IsNullOrWhiteSpace(unsupportedBranch) and throw an ArgumentException or
ArgumentNullException with a clear parameter name if invalid, then assign
UnsupportedBranch only after validation so the aggregated rejection message
never contains an empty value.
src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs (1)

10-19: Guard patternIdentifier before storing it as contract data.

Line 18 stores patternIdentifier without validation; empty values make rejection diagnostics ambiguous.

Proposed fix
     public UnsupportedRelationshipPatternException(
         string relationshipPath,
         string patternIdentifier)
         : base(
             $"Unsupported relationship pattern '{patternIdentifier}' at '{relationshipPath}'. " +
             "See the v2 contract documentation for supported patterns.",
             relationshipPath)
     {
+        ArgumentException.ThrowIfNullOrWhiteSpace(patternIdentifier);
         PatternIdentifier = patternIdentifier;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs`
around lines 10 - 19, The constructor for
UnsupportedRelationshipPatternException stores patternIdentifier directly into
the exception contract data without validation; add a guard in the
UnsupportedRelationshipPatternException constructor to validate
patternIdentifier (throw ArgumentNullException or ArgumentException for
null/empty/whitespace) before assigning PatternIdentifier and before any
contract-storage logic so consumers get a clear rejection diagnostic; update the
constructor that currently sets PatternIdentifier to perform this check and
throw a descriptive argument exception mentioning "patternIdentifier" when
invalid.
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs (1)

20-23: Add null guard before metadata dereference.

A defensive check avoids an ambiguous NRE at Line 22 if a null foreign key is ever passed.

♻️ Suggested hardening
 public static bool IsRequiredDependent(IForeignKey foreignKey)
 {
+    ArgumentNullException.ThrowIfNull(foreignKey);
     return foreignKey.IsRequired;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`
around lines 20 - 23, IsRequiredDependent currently dereferences
foreignKey.IsRequired without validation; add a null guard at the start of the
method (in the IsRequiredDependent method) to validate the foreignKey parameter
and throw an ArgumentNullException (include the parameter name) if it's null so
the method fails fast with an explicit error instead of producing an ambiguous
NRE.
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs (1)

17-23: Add defensive argument guards at the strategy boundary.

If upstream orchestration ever passes null, failure at Line 19 will be a less-informative NRE. Guarding inputs here improves diagnostics.

♻️ Suggested hardening
 public static void RemoveDependent(DbContext context, ReferenceEntry existingNavigation)
 {
+    ArgumentNullException.ThrowIfNull(context);
+    ArgumentNullException.ThrowIfNull(existingNavigation);
+
     var existingValue = existingNavigation.CurrentValue;
     if (existingValue is null)
         return;

     context.Remove(existingValue);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`
around lines 17 - 23, RequiredOneToOneStrategy.RemoveDependent currently
dereferences parameters without validation; add defensive argument guards at the
strategy boundary by validating both context and existingNavigation are not null
and throwing ArgumentNullException with appropriate param names (use
nameof(context) and nameof(existingNavigation)) at the top of the
RemoveDependent method before accessing existingNavigation.CurrentValue; keep
the existing null-check for existingNavigation.CurrentValue as-is.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/ManyToManySafetyContractTests.cs (1)

13-14: Redundant [Collection] attribute.

The [Collection(IntegrationTestCollection.Name)] attribute is already applied to IntegrationTestBase (Line 10 in that file). This attribute on the derived class is redundant.

Proposed fix
-[Collection(IntegrationTestCollection.Name)]
 public class ManyToManySafetyContractTests : IntegrationTestBase
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/ManyToManySafetyContractTests.cs`
around lines 13 - 14, The [Collection(IntegrationTestCollection.Name)] attribute
on the ManyToManySafetyContractTests class is redundant because
IntegrationTestBase already declares it; remove the attribute from the
ManyToManySafetyContractTests class declaration so the class simply reads
"public class ManyToManySafetyContractTests : IntegrationTestBase", and run
tests to confirm no behavior change.
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs (1)

48-75: Consider edge case: context.Find may return stale data if not already tracked.

When context.Find returns a database-loaded entity (Line 55), and you immediately call SetValues (Line 60), this works correctly. However, if the entity was already tracked with modifications from a previous operation within the same context lifetime, those modifications would be overwritten.

This is likely acceptable given the expected usage pattern (fresh context per operation), but worth noting for future maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`
around lines 48 - 75, context.Find can return an already-tracked entity whose
in-memory changes would be clobbered by the unconditional
context.Entry(knownEntity).CurrentValues.SetValues(updatedItem); modify the
logic inside the PureManyToManyStrategy flow to first inspect the tracked state
(via context.Entry(knownEntity).State) and only call SetValues when the entity
was not previously modified (e.g., State is Unchanged or Detached); otherwise
either merge selective properties or skip SetValues and just
AddToCollection(knownEntity) so you don’t overwrite local changes from the same
DbContext.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs (1)

33-34: Unused variable policyVersion.

The policyVersion variable is captured but never used in the test. Either use it in an assertion or remove it.

Proposed fix
         existing.Policy.Should().NotBeNull("seed data includes a CoursePolicy");
-        var policyVersion = existing.Policy!.PolicyVersion;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs`
around lines 33 - 34, The local variable policyVersion (assigned from
existing.Policy!.PolicyVersion) is unused; either remove that assignment or
assert something with it. Locate RequiredOneToOneContractTests and the lines
referencing existing.Policy and policyVersion, then either delete the
policyVersion assignment, or replace it with an assertion using policyVersion
(for example assert the expected PolicyVersion from seed data against
existing.Policy!.PolicyVersion) so the variable is consumed.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs (1)

84-125: Single-context pattern may cause change tracker interference.

Unlike Detects_added_skip_navigation_entries, this test uses a single context for seed, act, and assert phases. While this works with InMemory provider, it doesn't test the same code paths as production (where detached graphs are compared against freshly-loaded tracked entities).

Also, Line 94: CatalogId = Guid.NewGuid() references a non-existent catalog. InMemory provider doesn't enforce FK constraints, but this could mask real issues.

For consistency, consider restructuring to match the multi-context pattern used in the first test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`
around lines 84 - 125, The test Detects_removed_skip_navigation_entries uses a
single DbContext (CreateInMemoryContext) for arrange/act/assert which can let
the change tracker hide bugs; refactor it to follow the multi-context pattern
used in Detects_added_skip_navigation_entries by seeding data in one context
(ctx seed), then opening a fresh context to load existing via Include(c =>
c.Tags) into a tracked entity (existing), build an updated detached Course
object (updated) with the same CatalogId as the seeded course (do not call
CatalogId = Guid.NewGuid()), call InsertUpdateOrDeleteGraph(updated, existing)
on the fresh context, SaveChangesAsync, and finally verify results using a new
fresh context that Includes Tags so FK/catalog consistency is preserved and the
test exercises detached-vs-tracked logic.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs (1)

50-59: Consider adding timeout or retry for schema reset operations.

EnsureDeletedAsync and EnsureCreatedAsync can be slow or fail transiently when the SQL Server container is under load. For robustness in CI environments, consider wrapping with a command timeout or retry policy.

This is a nice-to-have for CI reliability; the current implementation is functionally correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
around lines 50 - 59, Wrap ResetSchemaAsync's calls to
context.Database.EnsureDeletedAsync and EnsureCreatedAsync with a
retry-and-timeout strategy to handle transient SQL Server container load; for
example, acquire the DbContext from CreateContext(connectionString) and retry
the EnsureDeletedAsync/EnsureCreatedAsync sequence a few times with short delays
and per-call cancellation token timeouts, or use a Polly retry policy that
retries on transient exceptions and enforces a per-attempt timeout—make sure to
dispose the context between retries and surface a clear error if retries are
exhausted.
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs (1)

68-77: The reflection-based approach works correctly for standard collection types; consider aligning with PureManyToManyStrategy for consistency.

GetMethod("Add") successfully retrieves the method for List<T>, HashSet<T>, and other standard collection types—each has exactly one public Add method. The current implementation is not broken.

However, for consistency and maintainability, adopt the same interface-based lookup pattern used in PureManyToManyStrategy.ExecuteCollectionOperation: check for IList first, then search GetInterfaces() for ICollection<> and retrieve the method from the interface rather than the concrete type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs`
around lines 68 - 77, The AddToCollection method currently uses concrete-type
reflection (GetMethod("Add")); change it to follow the pattern in
PureManyToManyStrategy.ExecuteCollectionOperation: if navigation.CurrentValue
implements IList use that directly, otherwise find an implemented generic
ICollection<> via navigation.CurrentValue.GetType().GetInterfaces(), get the Add
method from the ICollection<> interface (not the concrete type), and invoke that
method with the item; keep the existing InvalidOperationException messages but
replace the GetMethod("Add") lookup and invocation in AddToCollection with the
IList/ICollection<> detection and interface-based Add invocation.
src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs (1)

29-42: Consider consistent error handling between tracked and detached key extraction.

The tracked overload (GetKeyValues(EntityEntry)) throws InvalidOperationException when the entity lacks a primary key, but the detached overload silently returns an empty array. This inconsistency could mask configuration errors.

Consider throwing for consistency
     public static object[] GetKeyValues(DbContext context, object entity)
     {
         var entityType = context.Model.FindEntityType(entity.GetType());
         if (entityType is null)
-            return [];
+            throw new InvalidOperationException(
+                $"Entity type '{entity.GetType().Name}' is not part of the model.");

         var primaryKey = entityType.FindPrimaryKey();
         if (primaryKey is null)
-            return [];
+            throw new InvalidOperationException(
+                $"Entity '{entityType.ClrType.Name}' does not define a primary key.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs`
around lines 29 - 42, GetKeyValues(DbContext, object) currently returns an empty
array when the entity type or primary key is missing; change it to mirror the
tracked overload GetKeyValues(EntityEntry) by throwing an
InvalidOperationException instead of returning [] when
context.Model.FindEntityType(entity.GetType()) or entityType.FindPrimaryKey() is
null, and keep the rest of the method (calling GetRequiredDetachedKeyValue and
ToArray) the same so error handling is consistent between tracked and detached
key extraction.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PureManyToManyContractTests.cs (1)

145-179: Verify intended behavior: updating shared entity through association.

This test validates that updating a TopicTag label through the association propagates the change. However, if Tag1 is linked to multiple courses, this update affects all of them—which may or may not be the intended behavior.

Consider documenting this behavior explicitly in either the test name or a comment, as it represents a potentially surprising side effect for API consumers.

Consider clarifying the test intent
     [Fact]
-    public async Task Update_existing_related_entity_through_association_updates_entity_state()
+    public async Task Update_existing_related_entity_through_association_updates_entity_globally()
     {
+        // Note: This updates the shared TopicTag entity, affecting all courses linked to it.
+        // This is expected behavior for many-to-many relationships where the related entity
+        // is a shared principal, not an owned dependent.
+
         // Arrange
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PureManyToManyContractTests.cs`
around lines 145 - 179, The test
Update_existing_related_entity_through_association_updates_entity_state
currently updates a shared TopicTag (TopicTag with Id SeedData.Tag1Id) via the
association which will mutate that tag globally across all courses; update the
test to make the intent explicit by either renaming the test to indicate
shared-entity propagation (e.g., ..._updates_shared_entity_globally) or add a
concise comment above the arrange/assert sections explaining that updating a
TopicTag through InsertUpdateOrDeleteGraph will update the underlying TopicTag
for all courses that reference it, and optionally add an extra assert that
verifies another course referencing SeedData.Tag1Id also sees the changed Label
to codify the observed behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/dotnet-core.yml:
- Around line 21-24: Replace mutable action tags with immutable commit SHAs:
update the uses entries for actions/checkout and actions/setup-dotnet (and the
other occurrences of those same action identifiers later in the file) so they
reference the full commit SHA instead of `@v6` or `@v5`; find the uses:
actions/checkout@v6 and uses: actions/setup-dotnet@v5 entries and pin each to
the corresponding full commit SHA from the GitHub repository release (e.g.,
actions/checkout@<full-commit-sha>) to prevent tag retargeting.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PayloadAssociationContractTests.cs`:
- Around line 19-25: The test class PayloadAssociationContractTests is missing
an override of InitializeAsync which leads to shared schema state; add an async
override of InitializeAsync in the PayloadAssociationContractTests class that
resets/initializes the database/schema (matching the pattern used in other
integration test classes) and call any cleanup/setup helpers there, then remove
the per-test SeedData.SeedFullScenarioAsync calls from each test (including
Add_new_assignment_creates_association_entity_with_payload) so tests rely on the
centralized initialization and remain isolated.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/OptionalOneToOneContractTests.cs`:
- Around line 13-16: The test class OptionalOneToOneContractTests is missing the
collection attribute required for shared fixture lifecycle; add
[Collection(IntegrationTestCollection.Name)] above the
OptionalOneToOneContractTests class declaration so it uses the
IntegrationTestCollection fixture consistently with other integration tests.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs`:
- Around line 12-15: Add the missing test collection attribute to the
RequiredOneToOneContractTests class so the SqlServerContainerFixture is shared;
specifically, annotate the RequiredOneToOneContractTests class with
[Collection(IntegrationTestCollection.Name)] (same as
UnloadedNavigationMutationTests) so the test runner groups it with the
integration collection.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/Rejection/UnloadedNavigationMutationTests.cs`:
- Around line 14-17: The test class UnloadedNavigationMutationTests is missing
the collection attribute used for fixture sharing; add
[Collection(IntegrationTestCollection.Name)] above the
UnloadedNavigationMutationTests class declaration so it uses the same SQL Server
container fixture and test isolation as other tests that inherit
IntegrationTestBase (e.g., PayloadAssociationContractTests).

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs`:
- Around line 11-12: The MsSqlContainer instantiation uses a mutable image tag
"mcr.microsoft.com/mssql/server:2022-latest" in the private field _container
(constructed via MsSqlBuilder), which can cause flaky tests; change it to a
fixed immutable tag like "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-20.04"
or make the image configurable by reading an environment variable (e.g.,
SQL_SERVER_IMAGE) with that immutable tag as the default; update the
MsSqlBuilder call that constructs _container to use the chosen value so CI is
deterministic while allowing opt-in overrides.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs`:
- Around line 10-14: The GraphUpdateException constructor currently allows
null/whitespace for relationshipPath; update the GraphUpdateException(string
message, string relationshipPath) constructor to validate relationshipPath
(throw ArgumentNullException if null and ArgumentException if empty/whitespace)
before assigning to the RelationshipPath property so the exception always
carries a non-empty path for diagnostics.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs`:
- Around line 44-50: KeysEqual currently calls keys1.SequenceEqual(keys2) which
uses object.Equals and will treat byte[] keys by reference, causing false
negatives for equal byte[] contents; update EntityKeyHelper.KeysEqual to
explicitly handle byte[] elements: check nulls and lengths first, then for each
index if both elements are byte[] compare their contents (e.g., call
Enumerable.SequenceEqual on cast (byte[]) arrays or use
StructuralComparisons.StructuralEqualityComparer), otherwise fall back to
object.Equals; ensure symmetric handling when one is byte[] and the other is
not.

In
`@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs`:
- Around line 280-296: IsPayloadJoinEntity currently only checks that the PK is
composite and that all PK properties are also FK properties; update the
implementation to match the docstring by also verifying the presence of at least
one payload property (a property on the IEntityType that is not part of the
primary key and is not present in any of the entity's foreign key property
lists). Concretely, in IsPayloadJoinEntity after computing primaryKey and
foreignKeys, compute the set of FK properties (union of
foreignKeys.SelectMany(fk => fk.Properties)), then check for any property in
entityType.GetProperties() that is not in primaryKey.Properties and not in that
FK set; only return true if primaryKey exists, has count >=2, all PK parts are
FKs, and such a non-key/non-FK property exists. Ensure method still returns
false for pure join entities.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs`:
- Around line 18-52: Add explicit null-argument guards at the top of
DetachDependent, AttachDependent and ReplaceDependent: validate context and
existingNavigation in all three methods and throw ArgumentNullException if
either is null; additionally validate dependent in AttachDependent and
ReplaceDependent (throw ArgumentNullException if null) so callers get
deterministic, actionable failures instead of implicit dereference exceptions
when using DetachDependent, AttachDependent or ReplaceDependent.

---

Nitpick comments:
In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/ManyToManySafetyContractTests.cs`:
- Around line 13-14: The [Collection(IntegrationTestCollection.Name)] attribute
on the ManyToManySafetyContractTests class is redundant because
IntegrationTestBase already declares it; remove the attribute from the
ManyToManySafetyContractTests class declaration so the class simply reads
"public class ManyToManySafetyContractTests : IntegrationTestBase", and run
tests to confirm no behavior change.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PureManyToManyContractTests.cs`:
- Around line 145-179: The test
Update_existing_related_entity_through_association_updates_entity_state
currently updates a shared TopicTag (TopicTag with Id SeedData.Tag1Id) via the
association which will mutate that tag globally across all courses; update the
test to make the intent explicit by either renaming the test to indicate
shared-entity propagation (e.g., ..._updates_shared_entity_globally) or add a
concise comment above the arrange/assert sections explaining that updating a
TopicTag through InsertUpdateOrDeleteGraph will update the underlying TopicTag
for all courses that reference it, and optionally add an extra assert that
verifies another course referencing SeedData.Tag1Id also sees the changed Label
to codify the observed behavior.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs`:
- Around line 33-34: The local variable policyVersion (assigned from
existing.Policy!.PolicyVersion) is unused; either remove that assignment or
assert something with it. Locate RequiredOneToOneContractTests and the lines
referencing existing.Policy and policyVersion, then either delete the
policyVersion assignment, or replace it with an assertion using policyVersion
(for example assert the expected PolicyVersion from seed data against
existing.Policy!.PolicyVersion) so the variable is consumed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj`:
- Around line 12-15: Replace the floating wildcard package versions with exact,
pinned versions to ensure deterministic CI restores: update the PackageReference
entries for FluentAssertions, Microsoft.EntityFrameworkCore.SqlServer, and
Testcontainers.MsSql (the elements with Include="FluentAssertions",
Include="Microsoft.EntityFrameworkCore.SqlServer",
Include="Testcontainers.MsSql") to use explicit version numbers instead of
"8.*", "10.*", and "4.*" respectively (leave Microsoft.NET.Test.Sdk as-is if
already pinned); commit the csproj change so CI always restores the specified
exact package versions.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`:
- Around line 50-59: Wrap ResetSchemaAsync's calls to
context.Database.EnsureDeletedAsync and EnsureCreatedAsync with a
retry-and-timeout strategy to handle transient SQL Server container load; for
example, acquire the DbContext from CreateContext(connectionString) and retry
the EnsureDeletedAsync/EnsureCreatedAsync sequence a few times with short delays
and per-call cancellation token timeouts, or use a Polly retry policy that
retries on transient exceptions and enforces a per-attempt timeout—make sure to
dispose the context between retries and surface a clear error if retries are
exhausted.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`:
- Around line 84-125: The test Detects_removed_skip_navigation_entries uses a
single DbContext (CreateInMemoryContext) for arrange/act/assert which can let
the change tracker hide bugs; refactor it to follow the multi-context pattern
used in Detects_added_skip_navigation_entries by seeding data in one context
(ctx seed), then opening a fresh context to load existing via Include(c =>
c.Tags) into a tracked entity (existing), build an updated detached Course
object (updated) with the same CatalogId as the seeded course (do not call
CatalogId = Guid.NewGuid()), call InsertUpdateOrDeleteGraph(updated, existing)
on the fresh context, SaveChangesAsync, and finally verify results using a new
fresh context that Includes Tags so FK/catalog consistency is preserved and the
test exercises detached-vs-tracked logic.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs`:
- Around line 11-20: The constructor AmbiguousOwnershipSemanticsException should
validate the missingDetail parameter and reject null/whitespace to preserve
exception integrity; inside the AmbiguousOwnershipSemanticsException(string
relationshipPath, string missingDetail) constructor, check
string.IsNullOrWhiteSpace(missingDetail) and throw an ArgumentException or
ArgumentNullException with a clear message (including the parameter name
"missingDetail") before calling base, ensuring the public MissingDetail property
is only assigned valid non-empty text.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs`:
- Around line 11-20: The constructor PartialMutationNotAllowedException should
validate that the incoming unsupportedBranch is not null/empty/whitespace before
assigning; update the PartialMutationNotAllowedException(string
relationshipPath, string unsupportedBranch) constructor to check
string.IsNullOrWhiteSpace(unsupportedBranch) and throw an ArgumentException or
ArgumentNullException with a clear parameter name if invalid, then assign
UnsupportedBranch only after validation so the aggregated rejection message
never contains an empty value.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs`:
- Around line 12-23: The UnsupportedNavigationMutatedException constructor
stores the unvalidated parameter relationshipType into the RelationshipType
property; validate that relationshipType is not null/empty (e.g., throw
ArgumentNullException or ArgumentException after trimming) before assigning it.
Update the UnsupportedNavigationMutatedException(string relationshipPath, string
relationshipType) constructor to check relationshipType (and relationshipPath if
desired) and throw an appropriate exception for invalid values, then assign
RelationshipType only after validation so error payloads remain dependable.

In
`@src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs`:
- Around line 10-19: The constructor for UnsupportedRelationshipPatternException
stores patternIdentifier directly into the exception contract data without
validation; add a guard in the UnsupportedRelationshipPatternException
constructor to validate patternIdentifier (throw ArgumentNullException or
ArgumentException for null/empty/whitespace) before assigning PatternIdentifier
and before any contract-storage logic so consumers get a clear rejection
diagnostic; update the constructor that currently sets PatternIdentifier to
perform this check and throw a descriptive argument exception mentioning
"patternIdentifier" when invalid.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs`:
- Around line 29-42: GetKeyValues(DbContext, object) currently returns an empty
array when the entity type or primary key is missing; change it to mirror the
tracked overload GetKeyValues(EntityEntry) by throwing an
InvalidOperationException instead of returning [] when
context.Model.FindEntityType(entity.GetType()) or entityType.FindPrimaryKey() is
null, and keep the rest of the method (calling GetRequiredDetachedKeyValue and
ToArray) the same so error handling is consistent between tracked and detached
key extraction.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`:
- Line 16: The Errors property currently returns the mutable backing _errors
list directly; change the getter to return an immutable view (e.g. wrap _errors
with ReadOnlyCollection via _errors.AsReadOnly() or return a ReadOnlyCollection
cached field) so callers cannot cast back to List<T> and mutate state; keep
mutation confined to AddError and reference GraphUpdateException and
_errors/Errors/AddError to locate the change.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`:
- Around line 20-23: IsRequiredDependent currently dereferences
foreignKey.IsRequired without validation; add a null guard at the start of the
method (in the IsRequiredDependent method) to validate the foreignKey parameter
and throw an ArgumentNullException (include the parameter name) if it's null so
the method fails fast with an explicit error instead of producing an ambiguous
NRE.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs`:
- Around line 68-77: The AddToCollection method currently uses concrete-type
reflection (GetMethod("Add")); change it to follow the pattern in
PureManyToManyStrategy.ExecuteCollectionOperation: if navigation.CurrentValue
implements IList use that directly, otherwise find an implemented generic
ICollection<> via navigation.CurrentValue.GetType().GetInterfaces(), get the Add
method from the ICollection<> interface (not the concrete type), and invoke that
method with the item; keep the existing InvalidOperationException messages but
replace the GetMethod("Add") lookup and invocation in AddToCollection with the
IList/ICollection<> detection and interface-based Add invocation.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`:
- Around line 48-75: context.Find can return an already-tracked entity whose
in-memory changes would be clobbered by the unconditional
context.Entry(knownEntity).CurrentValues.SetValues(updatedItem); modify the
logic inside the PureManyToManyStrategy flow to first inspect the tracked state
(via context.Entry(knownEntity).State) and only call SetValues when the entity
was not previously modified (e.g., State is Unchanged or Detached); otherwise
either merge selective properties or skip SetValues and just
AddToCollection(knownEntity) so you don’t overwrite local changes from the same
DbContext.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`:
- Around line 17-23: RequiredOneToOneStrategy.RemoveDependent currently
dereferences parameters without validation; add defensive argument guards at the
strategy boundary by validating both context and existingNavigation are not null
and throwing ArgumentNullException with appropriate param names (use
nameof(context) and nameof(existingNavigation)) at the top of the
RemoveDependent method before accessing existingNavigation.CurrentValue; keep
the existing null-check for existingNavigation.CurrentValue as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50fc7f3f-cc0d-4bf9-ad23-1bb27c47fb06

📥 Commits

Reviewing files that changed from the base of the PR and between f71a103 and 34c7fd9.

📒 Files selected for processing (73)
  • .github/workflows/dotnet-core.yml
  • .gitignore
  • EfCore.UpdateGraph.sln
  • README.md
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseMentorAssignmentConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CoursePolicyConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/LearningCatalogConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorWorkspaceConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/TopicTagConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Diwink.Extensions.EntityFrameworkCore.TestModel.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/Course.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/CourseMentorAssignment.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/CoursePolicy.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/LearningCatalog.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/Mentor.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/MentorWorkspace.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Entities/TopicTag.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/TestDbContext.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/ManyToManySafetyContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PayloadAssociationContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PureManyToManyContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/OptionalOneToOneContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/Rejection/PartialMutationNotAllowedTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/Rejection/UnloadedNavigationMutationTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/Rejection/UnsupportedRelationshipPatternTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrapTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestBase.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestCollection.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/Diwink.Extensions.EntityFrameworkCore.Tests.Unit.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/DbContextExtensionsTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/NestedNavigationValidationTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/OperationGuardTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/UnsupportedRelationshipPatternTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/OneToOneOwnershipResolverTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Diwink.Extensions.EntityFrameworkCore.csproj
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnloadedNavigationMutationException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/RelatedEntityMutationService.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs
  • src/EFCore.UpdateGraph.slnx
  • src/FakeModel/FakeModel.csproj
  • src/FakeModel/Model.cs
  • src/UnitTests/FakeSchoolsDbContext.cs
  • src/UnitTests/FakeSchoolsDbContextFactory.cs
  • src/UnitTests/GraphUpdateTests.cs
  • src/UnitTests/Migrations/20200914083038_initialMigration.Designer.cs
  • src/UnitTests/Migrations/20200914083038_initialMigration.cs
  • src/UnitTests/Migrations/FakeSchoolsDbContextModelSnapshot.cs
  • src/UnitTests/TestHelpers.cs
  • src/UnitTests/UnitTests.csproj
  • src/UnitTests/VariousTests.cs
  • src/UnitTests/appsettings.json
💤 Files with no reviewable changes (13)
  • src/UnitTests/appsettings.json
  • EfCore.UpdateGraph.sln
  • src/FakeModel/FakeModel.csproj
  • src/UnitTests/TestHelpers.cs
  • src/UnitTests/FakeSchoolsDbContextFactory.cs
  • src/UnitTests/Migrations/FakeSchoolsDbContextModelSnapshot.cs
  • src/UnitTests/Migrations/20200914083038_initialMigration.Designer.cs
  • src/UnitTests/VariousTests.cs
  • src/FakeModel/Model.cs
  • src/UnitTests/UnitTests.csproj
  • src/UnitTests/GraphUpdateTests.cs
  • src/UnitTests/FakeSchoolsDbContext.cs
  • src/UnitTests/Migrations/20200914083038_initialMigration.cs

Comment thread .github/workflows/dotnet-core.yml Outdated
Comment on lines +19 to +25
[Fact]
public async Task Add_new_assignment_creates_association_entity_with_payload()
{
// Arrange
await using var seedCtx = CreateContext();
await SeedData.SeedFullScenarioAsync(seedCtx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing InitializeAsync override for test isolation.

Unlike other integration test classes, this test seeds data at the start of each test method (Line 23-24) without first resetting the schema. If tests run in parallel or in varying order, residual data from previous tests could cause failures.

Consider overriding InitializeAsync like other test classes:

Proposed fix
 public class PayloadAssociationContractTests : IntegrationTestBase
 {
     public PayloadAssociationContractTests(SqlServerContainerFixture fixture)
         : base(fixture) { }

+    public override async Task InitializeAsync()
+    {
+        await base.InitializeAsync();
+        await using var ctx = CreateContext();
+        await SeedData.SeedFullScenarioAsync(ctx);
+    }
+
     [Fact]
     public async Task Add_new_assignment_creates_association_entity_with_payload()
     {
-        // Arrange
-        await using var seedCtx = CreateContext();
-        await SeedData.SeedFullScenarioAsync(seedCtx);
-
         await using var ctx = CreateContext();

Then remove the seed calls from each test method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/ManyToMany/PayloadAssociationContractTests.cs`
around lines 19 - 25, The test class PayloadAssociationContractTests is missing
an override of InitializeAsync which leads to shared schema state; add an async
override of InitializeAsync in the PayloadAssociationContractTests class that
resets/initializes the database/schema (matching the pattern used in other
integration test classes) and call any cleanup/setup helpers there, then remove
the per-test SeedData.SeedFullScenarioAsync calls from each test (including
Add_new_assignment_creates_association_entity_with_payload) so tests rely on the
centralized initialization and remain isolated.

Comment on lines +13 to +16
public class OptionalOneToOneContractTests : IntegrationTestBase
{
public OptionalOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing [Collection] attribute for consistent fixture sharing.

Add [Collection(IntegrationTestCollection.Name)] for consistency with other integration tests and proper fixture lifecycle management.

Proposed fix
+[Collection(IntegrationTestCollection.Name)]
 public class OptionalOneToOneContractTests : IntegrationTestBase
 {
     public OptionalOneToOneContractTests(SqlServerContainerFixture fixture)
         : base(fixture) { }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class OptionalOneToOneContractTests : IntegrationTestBase
{
public OptionalOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
[Collection(IntegrationTestCollection.Name)]
public class OptionalOneToOneContractTests : IntegrationTestBase
{
public OptionalOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/OptionalOneToOneContractTests.cs`
around lines 13 - 16, The test class OptionalOneToOneContractTests is missing
the collection attribute required for shared fixture lifecycle; add
[Collection(IntegrationTestCollection.Name)] above the
OptionalOneToOneContractTests class declaration so it uses the
IntegrationTestCollection fixture consistently with other integration tests.

Comment on lines +12 to +15
public class RequiredOneToOneContractTests : IntegrationTestBase
{
public RequiredOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing [Collection] attribute for test collection grouping.

Similar to UnloadedNavigationMutationTests, this class should have the [Collection(IntegrationTestCollection.Name)] attribute to properly share the SqlServerContainerFixture.

Proposed fix
+[Collection(IntegrationTestCollection.Name)]
 public class RequiredOneToOneContractTests : IntegrationTestBase
 {
     public RequiredOneToOneContractTests(SqlServerContainerFixture fixture)
         : base(fixture) { }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class RequiredOneToOneContractTests : IntegrationTestBase
{
public RequiredOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
[Collection(IntegrationTestCollection.Name)]
public class RequiredOneToOneContractTests : IntegrationTestBase
{
public RequiredOneToOneContractTests(SqlServerContainerFixture fixture)
: base(fixture) { }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs`
around lines 12 - 15, Add the missing test collection attribute to the
RequiredOneToOneContractTests class so the SqlServerContainerFixture is shared;
specifically, annotate the RequiredOneToOneContractTests class with
[Collection(IntegrationTestCollection.Name)] (same as
UnloadedNavigationMutationTests) so the test runner groups it with the
integration collection.

Comment on lines +14 to +17
public class UnloadedNavigationMutationTests : IntegrationTestBase
{
public UnloadedNavigationMutationTests(SqlServerContainerFixture fixture)
: base(fixture) { }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing [Collection] attribute for test isolation.

Other integration test classes in this PR (e.g., PayloadAssociationContractTests) use [Collection(IntegrationTestCollection.Name)] to share the SQL Server container fixture across tests. This class inherits from IntegrationTestBase but lacks the collection attribute, which may cause fixture lifecycle issues or parallel execution conflicts.

Proposed fix
+[Collection(IntegrationTestCollection.Name)]
 public class UnloadedNavigationMutationTests : IntegrationTestBase
 {
     public UnloadedNavigationMutationTests(SqlServerContainerFixture fixture)
         : base(fixture) { }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class UnloadedNavigationMutationTests : IntegrationTestBase
{
public UnloadedNavigationMutationTests(SqlServerContainerFixture fixture)
: base(fixture) { }
[Collection(IntegrationTestCollection.Name)]
public class UnloadedNavigationMutationTests : IntegrationTestBase
{
public UnloadedNavigationMutationTests(SqlServerContainerFixture fixture)
: base(fixture) { }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/Rejection/UnloadedNavigationMutationTests.cs`
around lines 14 - 17, The test class UnloadedNavigationMutationTests is missing
the collection attribute used for fixture sharing; add
[Collection(IntegrationTestCollection.Name)] above the
UnloadedNavigationMutationTests class declaration so it uses the same SQL Server
container fixture and test isolation as other tests that inherit
IntegrationTestBase (e.g., PayloadAssociationContractTests).

Comment on lines +280 to +296
/// <summary>
/// Detects whether an entity type is a payload join entity (explicit many-to-many).
/// A payload join entity has a composite key where all key parts are also foreign keys,
/// AND it has additional non-key, non-FK properties (the payload).
/// </summary>
private static bool IsPayloadJoinEntity(IEntityType entityType)
{
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey is null || primaryKey.Properties.Count < 2)
return false;

var foreignKeys = entityType.GetForeignKeys().ToList();
var allKeyPropsAreFk = primaryKey.Properties.All(keyProp =>
foreignKeys.Any(fk => fk.Properties.Contains(keyProp)));

return allKeyPropsAreFk;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation/implementation mismatch in IsPayloadJoinEntity.

The docstring states the method checks for "additional non-key, non-FK properties (the payload)", but the implementation only verifies that all PK properties are also FK properties—it does not verify the presence of actual payload properties.

This means the current implementation would classify pure join tables (without payload) as "payload" entities if they use composite FK keys. In practice, EF Core pure M:M uses ISkipNavigation (handled earlier), but this could cause confusion for explicit join entities without payload.

Align implementation with documentation

Either update the docstring to match implementation:

     /// <summary>
-    /// Detects whether an entity type is a payload join entity (explicit many-to-many).
-    /// A payload join entity has a composite key where all key parts are also foreign keys,
-    /// AND it has additional non-key, non-FK properties (the payload).
+    /// Detects whether an entity type is an explicit join entity (payload many-to-many).
+    /// An explicit join entity has a composite key where all key parts are also foreign keys.
     /// </summary>

Or add payload property validation:

         return allKeyPropsAreFk;
+        
+        // Check for payload properties (non-key, non-FK properties)
+        var fkProperties = foreignKeys.SelectMany(fk => fk.Properties).ToHashSet();
+        var hasPayload = entityType.GetProperties()
+            .Any(p => !primaryKey.Properties.Contains(p) && !fkProperties.Contains(p) && !p.IsShadowProperty());
+        
+        return allKeyPropsAreFk && hasPayload;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs`
around lines 280 - 296, IsPayloadJoinEntity currently only checks that the PK is
composite and that all PK properties are also FK properties; update the
implementation to match the docstring by also verifying the presence of at least
one payload property (a property on the IEntityType that is not part of the
primary key and is not present in any of the entity's foreign key property
lists). Concretely, in IsPayloadJoinEntity after computing primaryKey and
foreignKeys, compute the set of FK properties (union of
foreignKeys.SelectMany(fk => fk.Properties)), then check for any property in
entityType.GetProperties() that is not in primaryKey.Properties and not in that
FK set; only return true if primaryKey exists, has count >=2, all PK parts are
FKs, and such a non-key/non-FK property exists. Ensure method still returns
false for pure join entities.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 5, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #14

Docstrings generation was requested by @WahidBitar.

* #13 (comment)

The following files were modified:

* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseMentorAssignmentConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CoursePolicyConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/LearningCatalogConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorWorkspaceConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/TopicTagConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/TestDbContext.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestBase.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnloadedNavigationMutationException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/RelatedEntityMutationService.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 5, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 5, 2026

✅ Created PR with unit tests: #15

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
.github/workflows/dotnet-core.yml (1)

14-66: Optional: add workflow concurrency to reduce overlapping publish attempts.

This is not a blocker, but concurrency control helps avoid duplicate runs on rapid successive pushes.

Suggested refinement
 name: .NET CI
+
+concurrency:
+  group: dotnet-ci-${{ github.ref }}
+  cancel-in-progress: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/dotnet-core.yml around lines 14 - 66, Add GitHub Actions
concurrency to the publish job to prevent overlapping publish runs: inside the
existing publish job (named "publish") add a concurrency block such as
"concurrency: { group: 'publish-${{ github.ref }}', cancel-in-progress: true }"
(or similar group key to suit your branch/tag naming) so new publish runs cancel
in-progress ones and avoid duplicate NuGet pushes.
src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs (1)

35-38: XML documentation is misleading for the multiple-errors case.

The doc states "Throws the first recorded error if any exist" but when multiple errors are recorded, the method throws a PartialMutationNotAllowedException (a different exception type), not the first error.

📝 Suggested documentation fix
     /// <summary>
-    /// Throws the first recorded error if any exist, enforcing all-or-nothing
-    /// rejection before mutations are applied to the change tracker.
+    /// Throws if any errors were recorded, enforcing all-or-nothing rejection
+    /// before mutations are applied to the change tracker. A single error is
+    /// rethrown directly; multiple errors are wrapped in a
+    /// <see cref="PartialMutationNotAllowedException"/>.
     /// </summary>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`
around lines 35 - 38, Update the XML doc on OperationGuard.ThrowIfErrors to
accurately describe its behavior: explain that if one error is recorded the
method rethrows that single recorded exception, but if multiple errors are
recorded it throws a PartialMutationNotAllowedException containing aggregated
error information; reference the OperationGuard class and the
PartialMutationNotAllowedException type in the summary so the documentation
matches the implemented behavior.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs (2)

199-249: Use a fresh verify context for assertion consistency.

This test asserts using the same context used for arrange/act, while other tests use fresh verification contexts. Aligning this test to the same pattern reduces state-carryover risk and keeps semantics consistent across the suite.

Proposed structure alignment
     public async Task Detects_added_payload_association_entities()
     {
-        // Arrange
-        await using var ctx = CreateInMemoryContext();
+        var dbName = Guid.NewGuid().ToString();
+
+        // Arrange
+        await using var ctx = CreateInMemoryContext(dbName);
         var mentor = new Mentor { Id = Guid.NewGuid(), DisplayName = "M1", Status = "Active" };
         var course = new Course
         {
@@
         ctx.InsertUpdateOrDeleteGraph(updated, existing);
         await ctx.SaveChangesAsync();

         // Assert
-        var assignments = await ctx.CourseMentorAssignments
+        await using var verifyCtx = CreateInMemoryContext(dbName);
+        var assignments = await verifyCtx.CourseMentorAssignments
             .Where(a => a.CourseId == course.Id)
             .ToListAsync();
         assignments.Should().HaveCount(1);
         assignments[0].Role.Should().Be("Lead");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`
around lines 199 - 249, The test Detects_added_payload_association_entities uses
the same DbContext (ctx) for Arrange/Act and Assert which can leak tracked
state; change the assertion phase to use a fresh verification context created
via CreateInMemoryContext() so you query persisted state cleanly. Specifically,
after await ctx.SaveChangesAsync() and before assertions, dispose/stop using the
original ctx and create a new var verifyCtx = CreateInMemoryContext(); then
query verifyCtx.CourseMentorAssignments (instead of ctx.CourseMentorAssignments)
to assert the created CourseMentorAssignment; keep usage of
InsertUpdateOrDeleteGraph and existing test data unchanged.

74-81: Strengthen assertion to validate membership, not only count.

Line 80 only checks cardinality; it can pass with the wrong tag identities. Assert the resulting tag IDs as well.

Proposed assertion hardening
             var result = await verifyCtx.Courses
                 .Include(c => c.Tags)
                 .FirstAsync(c => c.Id == courseId);
-            result.Tags.Should().HaveCount(2);
+            result.Tags.Should().HaveCount(2);
+            result.Tags.Select(t => t.Id).Should().BeEquivalentTo([tag1Id, tag2Id]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`
around lines 74 - 81, The test currently only asserts
result.Tags.Should().HaveCount(2) which can hide wrong identities; update the
assertion in ManyToManyDiffStrategyTests (inside the verify block using
CreateInMemoryContext and verifyCtx.Courses.Include(c =>
c.Tags).FirstAsync(...)) to assert the tag identities too — e.g., collect
result.Tags.Select(t => t.Id) and assert it matches the expected tag id set (use
FluentAssertions' BeEquivalentTo or Should().Contain/OnlyContain with the known
tagId variables used earlier in the test, e.g., expectedTagId1/expectedTagId2)
so the test verifies membership not just count.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/EntityKeyHelperTests.cs`:
- Line 48: The declaration private sealed class UnmappedEntity; uses a
semicolon-only class declaration which can be invalid depending on C# language
version; change the UnmappedEntity declaration to have an explicit empty body
(replace the semicolon-only declaration with a class body using braces) so the
class compiles across all C# versions, updating the UnmappedEntity type used in
EntityKeyHelperTests accordingly.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`:
- Around line 141-196: The test
Linking_existing_related_entity_does_not_clobber_modified_tracked_state
currently seeds the course with Tags = [] so it never exercises the
"already-linked" clobber path; add a new test (or modify this one) that seeds
the persisted Course with the TopicTag already present in existing.Tags (e.g. in
the initial seed block set Course.Tags to a collection containing the TopicTag
with Id = tagId), then in the act block fetch existing via ctx.Courses.Include(c
=> c.Tags), modify the tracked tag (trackedTag.Label = "Locally Modified" and
assert EntityState.Modified), call ctx.InsertUpdateOrDeleteGraph(updated,
existing) with updated.Tags containing a detached copy (same Id but different
Label), SaveChangesAsync, and finally verify the stored tag label remains
"Locally Modified" to ensure InsertUpdateOrDeleteGraph does not clobber a
locally modified tracked related entity.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`:
- Around line 47-52: The current code passes the first error path as the primary
relationshipPath and includes it again inside allPaths, causing a duplicate in
the exception message; change the allPaths assembly to exclude the primary path
(use _errors.Skip(1).Select(e => e.RelationshipPath) when building allPaths) and
pass the primary as before to PartialMutationNotAllowedException (or, if the
constructor expects a non-empty unsupportedBranch, fall back to the primary when
the skipped collection is empty) so the message shows the primary path once and
any additional unsupported branches separately.

---

Nitpick comments:
In @.github/workflows/dotnet-core.yml:
- Around line 14-66: Add GitHub Actions concurrency to the publish job to
prevent overlapping publish runs: inside the existing publish job (named
"publish") add a concurrency block such as "concurrency: { group: 'publish-${{
github.ref }}', cancel-in-progress: true }" (or similar group key to suit your
branch/tag naming) so new publish runs cancel in-progress ones and avoid
duplicate NuGet pushes.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`:
- Around line 199-249: The test Detects_added_payload_association_entities uses
the same DbContext (ctx) for Arrange/Act and Assert which can leak tracked
state; change the assertion phase to use a fresh verification context created
via CreateInMemoryContext() so you query persisted state cleanly. Specifically,
after await ctx.SaveChangesAsync() and before assertions, dispose/stop using the
original ctx and create a new var verifyCtx = CreateInMemoryContext(); then
query verifyCtx.CourseMentorAssignments (instead of ctx.CourseMentorAssignments)
to assert the created CourseMentorAssignment; keep usage of
InsertUpdateOrDeleteGraph and existing test data unchanged.
- Around line 74-81: The test currently only asserts
result.Tags.Should().HaveCount(2) which can hide wrong identities; update the
assertion in ManyToManyDiffStrategyTests (inside the verify block using
CreateInMemoryContext and verifyCtx.Courses.Include(c =>
c.Tags).FirstAsync(...)) to assert the tag identities too — e.g., collect
result.Tags.Select(t => t.Id) and assert it matches the expected tag id set (use
FluentAssertions' BeEquivalentTo or Should().Contain/OnlyContain with the known
tagId variables used earlier in the test, e.g., expectedTagId1/expectedTagId2)
so the test verifies membership not just count.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`:
- Around line 35-38: Update the XML doc on OperationGuard.ThrowIfErrors to
accurately describe its behavior: explain that if one error is recorded the
method rethrows that single recorded exception, but if multiple errors are
recorded it throws a PartialMutationNotAllowedException containing aggregated
error information; reference the OperationGuard class and the
PartialMutationNotAllowedException type in the summary so the documentation
matches the implemented behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3080411-44c4-4fcd-8d95-bce6e84080f5

📥 Commits

Reviewing files that changed from the base of the PR and between 34c7fd9 and 2d6c39d.

📒 Files selected for processing (23)
  • .github/workflows/dotnet-core.yml
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/EntityKeyHelperTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/ExceptionValidationTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/OperationGuardTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/RelationshipClassificationTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs
✅ Files skipped from review due to trivial changes (11)
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Contracts/OneToOne/RequiredOneToOneContractTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/OperationGuardTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs

.WithMessage("*does not exist in the current DbContext model*");
}

private sealed class UnmappedEntity;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential C# syntax incompatibility at Line 48 (class declaration with ;).

private sealed class UnmappedEntity; may fail to compile depending on the configured C# language version. Prefer an explicit empty body for compatibility.

Proposed fix
-    private sealed class UnmappedEntity;
+    private sealed class UnmappedEntity
+    {
+    }
Is `private sealed class UnmappedEntity;` valid C# syntax for regular class declarations? Please cite official Microsoft C# language documentation/spec and indicate which C# versions support it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/GraphDiff/EntityKeyHelperTests.cs`
at line 48, The declaration private sealed class UnmappedEntity; uses a
semicolon-only class declaration which can be invalid depending on C# language
version; change the UnmappedEntity declaration to have an explicit empty body
(replace the semicolon-only declaration with a class body using braces) so the
class compiles across all C# versions, updating the UnmappedEntity type used in
EntityKeyHelperTests accordingly.

Comment on lines +141 to +196
public async Task Linking_existing_related_entity_does_not_clobber_modified_tracked_state()
{
var dbName = Guid.NewGuid().ToString();
var catalogId = Guid.NewGuid();
var courseId = Guid.NewGuid();
var tagId = Guid.NewGuid();

{
await using var seedCtx = CreateInMemoryContext(dbName);
seedCtx.LearningCatalogs.Add(new LearningCatalog { Id = catalogId, Name = "Catalog" });
seedCtx.TopicTags.Add(new TopicTag { Id = tagId, Label = "Persisted" });
seedCtx.Courses.Add(new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = []
});
await seedCtx.SaveChangesAsync();
}

{
await using var ctx = CreateInMemoryContext(dbName);
var existing = await ctx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);

var trackedTag = await ctx.TopicTags.FirstAsync(t => t.Id == tagId);
trackedTag.Label = "Locally Modified";
ctx.ChangeTracker.DetectChanges();
ctx.Entry(trackedTag).State.Should().Be(EntityState.Modified);

var updated = new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = [new TopicTag { Id = tagId, Label = "Detached Copy" }]
};

ctx.InsertUpdateOrDeleteGraph(updated, existing);
await ctx.SaveChangesAsync();
}

{
await using var verifyCtx = CreateInMemoryContext(dbName);
var result = await verifyCtx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);

result.Tags.Should().ContainSingle(t => t.Id == tagId);
result.Tags.Single().Label.Should().Be("Locally Modified");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Coverage gap: this test does not hit the already-linked clobber path.

Despite the name, Line 158 seeds Tags = [], so this only validates linking a pre-existing store entity. It does not validate the branch where the related entity is already in existing.Tags and locally modified before diff. Add a dedicated test for that case to prevent false confidence on tracked-state preservation.

Proposed additional test case
+    [Fact]
+    public async Task Existing_linked_related_entity_does_not_clobber_modified_tracked_state()
+    {
+        var dbName = Guid.NewGuid().ToString();
+        var catalogId = Guid.NewGuid();
+        var courseId = Guid.NewGuid();
+        var tagId = Guid.NewGuid();
+
+        await using (var seedCtx = CreateInMemoryContext(dbName))
+        {
+            seedCtx.LearningCatalogs.Add(new LearningCatalog { Id = catalogId, Name = "Catalog" });
+            var tag = new TopicTag { Id = tagId, Label = "Persisted" };
+            seedCtx.Courses.Add(new Course
+            {
+                Id = courseId,
+                CatalogId = catalogId,
+                Title = "Test",
+                Code = "T-001",
+                Tags = [tag]
+            });
+            await seedCtx.SaveChangesAsync();
+        }
+
+        await using (var ctx = CreateInMemoryContext(dbName))
+        {
+            var existing = await ctx.Courses.Include(c => c.Tags).FirstAsync(c => c.Id == courseId);
+            var trackedTag = existing.Tags.Single(t => t.Id == tagId);
+            trackedTag.Label = "Locally Modified";
+            ctx.ChangeTracker.DetectChanges();
+
+            var updated = new Course
+            {
+                Id = courseId,
+                CatalogId = catalogId,
+                Title = "Test",
+                Code = "T-001",
+                Tags = [new TopicTag { Id = tagId, Label = "Detached Copy" }]
+            };
+
+            ctx.InsertUpdateOrDeleteGraph(updated, existing);
+            await ctx.SaveChangesAsync();
+        }
+
+        await using (var verifyCtx = CreateInMemoryContext(dbName))
+        {
+            var result = await verifyCtx.Courses.Include(c => c.Tags).FirstAsync(c => c.Id == courseId);
+            result.Tags.Single().Label.Should().Be("Locally Modified");
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async Task Linking_existing_related_entity_does_not_clobber_modified_tracked_state()
{
var dbName = Guid.NewGuid().ToString();
var catalogId = Guid.NewGuid();
var courseId = Guid.NewGuid();
var tagId = Guid.NewGuid();
{
await using var seedCtx = CreateInMemoryContext(dbName);
seedCtx.LearningCatalogs.Add(new LearningCatalog { Id = catalogId, Name = "Catalog" });
seedCtx.TopicTags.Add(new TopicTag { Id = tagId, Label = "Persisted" });
seedCtx.Courses.Add(new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = []
});
await seedCtx.SaveChangesAsync();
}
{
await using var ctx = CreateInMemoryContext(dbName);
var existing = await ctx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);
var trackedTag = await ctx.TopicTags.FirstAsync(t => t.Id == tagId);
trackedTag.Label = "Locally Modified";
ctx.ChangeTracker.DetectChanges();
ctx.Entry(trackedTag).State.Should().Be(EntityState.Modified);
var updated = new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = [new TopicTag { Id = tagId, Label = "Detached Copy" }]
};
ctx.InsertUpdateOrDeleteGraph(updated, existing);
await ctx.SaveChangesAsync();
}
{
await using var verifyCtx = CreateInMemoryContext(dbName);
var result = await verifyCtx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);
result.Tags.Should().ContainSingle(t => t.Id == tagId);
result.Tags.Single().Label.Should().Be("Locally Modified");
}
}
public async Task Linking_existing_related_entity_does_not_clobber_modified_tracked_state()
{
var dbName = Guid.NewGuid().ToString();
var catalogId = Guid.NewGuid();
var courseId = Guid.NewGuid();
var tagId = Guid.NewGuid();
{
await using var seedCtx = CreateInMemoryContext(dbName);
seedCtx.LearningCatalogs.Add(new LearningCatalog { Id = catalogId, Name = "Catalog" });
seedCtx.TopicTags.Add(new TopicTag { Id = tagId, Label = "Persisted" });
seedCtx.Courses.Add(new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = []
});
await seedCtx.SaveChangesAsync();
}
{
await using var ctx = CreateInMemoryContext(dbName);
var existing = await ctx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);
var trackedTag = await ctx.TopicTags.FirstAsync(t => t.Id == tagId);
trackedTag.Label = "Locally Modified";
ctx.ChangeTracker.DetectChanges();
ctx.Entry(trackedTag).State.Should().Be(EntityState.Modified);
var updated = new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = [new TopicTag { Id = tagId, Label = "Detached Copy" }]
};
ctx.InsertUpdateOrDeleteGraph(updated, existing);
await ctx.SaveChangesAsync();
}
{
await using var verifyCtx = CreateInMemoryContext(dbName);
var result = await verifyCtx.Courses
.Include(c => c.Tags)
.FirstAsync(c => c.Id == courseId);
result.Tags.Should().ContainSingle(t => t.Id == tagId);
result.Tags.Single().Label.Should().Be("Locally Modified");
}
}
[Fact]
public async Task Existing_linked_related_entity_does_not_clobber_modified_tracked_state()
{
var dbName = Guid.NewGuid().ToString();
var catalogId = Guid.NewGuid();
var courseId = Guid.NewGuid();
var tagId = Guid.NewGuid();
await using (var seedCtx = CreateInMemoryContext(dbName))
{
seedCtx.LearningCatalogs.Add(new LearningCatalog { Id = catalogId, Name = "Catalog" });
var tag = new TopicTag { Id = tagId, Label = "Persisted" };
seedCtx.Courses.Add(new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = [tag]
});
await seedCtx.SaveChangesAsync();
}
await using (var ctx = CreateInMemoryContext(dbName))
{
var existing = await ctx.Courses.Include(c => c.Tags).FirstAsync(c => c.Id == courseId);
var trackedTag = existing.Tags.Single(t => t.Id == tagId);
trackedTag.Label = "Locally Modified";
ctx.ChangeTracker.DetectChanges();
var updated = new Course
{
Id = courseId,
CatalogId = catalogId,
Title = "Test",
Code = "T-001",
Tags = [new TopicTag { Id = tagId, Label = "Detached Copy" }]
};
ctx.InsertUpdateOrDeleteGraph(updated, existing);
await ctx.SaveChangesAsync();
}
await using (var verifyCtx = CreateInMemoryContext(dbName))
{
var result = await verifyCtx.Courses.Include(c => c.Tags).FirstAsync(c => c.Id == courseId);
result.Tags.Single().Label.Should().Be("Locally Modified");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs`
around lines 141 - 196, The test
Linking_existing_related_entity_does_not_clobber_modified_tracked_state
currently seeds the course with Tags = [] so it never exercises the
"already-linked" clobber path; add a new test (or modify this one) that seeds
the persisted Course with the TopicTag already present in existing.Tags (e.g. in
the initial seed block set Course.Tags to a collection containing the TopicTag
with Id = tagId), then in the act block fetch existing via ctx.Courses.Include(c
=> c.Tags), modify the tracked tag (trackedTag.Label = "Locally Modified" and
assert EntityState.Modified), call ctx.InsertUpdateOrDeleteGraph(updated,
existing) with updated.Tags containing a detached copy (same Id but different
Label), SaveChangesAsync, and finally verify the stored tag label remains
"Locally Modified" to ensure InsertUpdateOrDeleteGraph does not clobber a
locally modified tracked related entity.

Comment on lines +47 to +52
// When multiple errors exist, wrap in PartialMutationNotAllowed
// with all unsupported branches listed for diagnostic clarity
var allPaths = string.Join(", ", _errors.Select(e => e.RelationshipPath));
throw new PartialMutationNotAllowedException(
_errors[0].RelationshipPath,
allPaths);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the PartialMutationNotAllowedException constructor to understand how
# the two path parameters are used in the exception message.

ast-grep --pattern $'class PartialMutationNotAllowedException {
  $$$
}'

# Also check for message formatting
rg -n -A10 'class PartialMutationNotAllowedException'

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 1582


Clarify intent of passing both the first path and all paths to the exception.

The constructor accepts relationshipPath (passed to base) and unsupportedBranch (embedded in message: "...detected at '{unsupportedBranch}'..."). Since allPaths includes the first path, it already contains _errors[0].RelationshipPath. Confirm whether:

  1. Both parameters are intentional (primary path for base exception, all paths in message), or
  2. The first path should be excluded from allPaths to avoid duplication in the displayed message.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`
around lines 47 - 52, The current code passes the first error path as the
primary relationshipPath and includes it again inside allPaths, causing a
duplicate in the exception message; change the allPaths assembly to exclude the
primary path (use _errors.Skip(1).Select(e => e.RelationshipPath) when building
allPaths) and pass the primary as before to PartialMutationNotAllowedException
(or, if the constructor expects a non-empty unsupportedBranch, fall back to the
primary when the skipped collection is empty) so the message shows the primary
path once and any additional unsupported branches separately.

…and graph update exceptions

Enhance test coverage by adding integration tests for the SeedData helper to validate distinct IDs, entity relationships, and in-memory database seeding. Introduce unit tests for `GraphUpdateException` derived types, ensuring robust constructor validations, message formatting, and inheritance chain testing. Remove deprecated `ManyToManyDiffStrategyTests` in favor of the new testing structure. Update dependencies in project files to include EF Core InMemory provider for test scenarios.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (3)
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs (1)

121-125: Consider narrowing the retryable exception scope.

InvalidOperationException is quite broad and could mask genuine programming errors. For SQL Server connectivity issues, SqlException typically covers transient failures. Consider whether InvalidOperationException is truly needed here, or if specific subtypes/messages should be checked.

That said, for test infrastructure against containerized SQL Server, this broad catch may be pragmatic given connection pool edge cases during container startup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
around lines 121 - 125, The IsRetryableSchemaException method currently treats
every InvalidOperationException as retryable which is too broad; narrow it by
removing the blanket InvalidOperationException check and instead only treat
InvalidOperationException as retryable when it matches known transient scenarios
(e.g., specific message/errno/inner-exception patterns caused by SQL
connectivity or pool initialization), or drop it entirely and rely on
SqlException/TimeoutException plus the cancellation/OperationCanceledException
branch; update IsRetryableSchemaException to check exception is SqlException or
TimeoutException or (cancellationSource.IsCancellationRequested && exception is
OperationCanceledException), and if you must keep InvalidOperationException, add
targeted checks for known transient messages/inner-exceptions in that function
(use the exception.Message or exception.InnerException type to identify
transient pool/startup errors).
src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs (1)

55-83: Consider batching context.Find calls for large collections.

Each unlinked entity in updatedItems triggers a context.Find() call (Line 63), which may query the database if the entity isn't tracked. For collections with many new links, this could cause N+1 query behavior.

For typical use cases with small collections, this is acceptable. If performance becomes a concern with larger graphs, consider pre-loading candidate entities in a single query.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`
around lines 55 - 83, The per-item context.Find call in PureManyToManyStrategy
(see usage of context.Find, updatedItem and updatedKeys) can cause N+1 queries
for large updatedItems collections; modify the logic to batch-load candidate
entities before the loop by collecting all primary key values for the target
entity type, querying the DbSet for those keys in a single query to build a
lookup (dictionary by key), then inside the loop use the lookup to resolve
knownEntity and only fall back to context.Find or Add when a key is missing;
keep ApplyValuesIfNotModified and AddToCollection usage but replace the repeated
context.Find calls with the preloaded lookup to avoid many DB roundtrips.
src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedDataStructureTests.cs (1)

53-57: Rename this test or assert the “catalog prefix” invariant.

The method name/comment says we're checking a shared catalog prefix, but the assertion only verifies that the two IDs differ. That mismatch makes the test intent harder to trust when it fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedDataStructureTests.cs`
around lines 53 - 57, The test Course1Id_and_Course2Id_share_same_catalog_prefix
currently only asserts
SeedData.Course1Id.Should().NotBe(SeedData.Course2Id);—either rename the test to
reflect that it only verifies inequality, or add an assertion that checks the
catalog prefix invariant: extract the catalog prefix from SeedData.Course1Id and
SeedData.Course2Id (using the same parsing logic used in your ID type) and
assert they are equal while still asserting the IDs are not equal; update the
test name if you choose to keep only the inequality check (e.g.,
CourseIds_are_distinct) or keep the existing name and add the prefix equality
assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`:
- Around line 53-58: The XML documentation for the DatabaseBootstrap method
contains a duplicated and unclosed <summary> tag (the first <summary> is not
closed), causing malformed XML comments; update the doc on the
DatabaseBootstrap/EnsureDatabaseSchemaCreated member so there is a single
well-formed <summary>...</summary> block (remove the extra <summary> line or
close it properly) and ensure the param description remains inside a proper
<param name="connectionString">...</param> tag.
- Around line 18-24: The XML doc for the method in DatabaseBootstrap contains
duplicate/malformed <summary> tags; open the documentation for the method that
"Creates a TestDbContext configured to use SQL Server..." (the method in class
DatabaseBootstrap that returns a TestDbContext and accepts a connectionString)
and merge the two <summary> blocks into a single well-formed
<summary>...</summary> describing the behavior, then ensure the existing <param>
and <returns> tags remain intact and properly closed so the XML doc compiles
without CS1570.
- Around line 67-73: The XML doc for the database-reset method contains a
duplicated and unclosed <summary> tag; fix the XML by consolidating the two
summary lines into a single well-formed XML comment (one <summary> ...
</summary>) for the method in DatabaseBootstrap (the method that "Deletes and
recreates the test database schema for the given connection string"), ensuring
the opening <summary> is closed and no duplicate <summary> tag remains so XML
documentation parses correctly.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs`:
- Around line 23-28: The XML doc for the seeding method in the SeedData class is
malformed due to a duplicate opening <summary> tag; fix it by consolidating the
two <summary> entries into a single well-formed XML summary block (one opening
<summary>, the combined description, then a single closing </summary>), and
ensure the <param name="context"> tag remains outside that summary and correctly
formed; update the comment above the seeding method in SeedData.cs so XML doc
generation warnings are resolved.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/Exceptions/GraphUpdateExceptionTests.cs`:
- Around line 223-229: The test currently asserts that
UnsupportedNavigationMutatedException(string.Empty, string.Empty) stores empty
strings, but per GraphUpdateException validation it should reject an empty
relationshipPath; update the test to assert that constructing
UnsupportedNavigationMutatedException with an empty relationshipPath throws the
appropriate validation exception (e.g., ArgumentException or the specific
validation exception used by GraphUpdateException) instead of expecting
successful storage; reference the UnsupportedNavigationMutatedException
constructor in the assertion.

In `@src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs`:
- Around line 12-35: Remove the duplicate XML doc block that repeats <summary>
and <param> tags for the extension method in DbContextExtensions (the doc
referencing updatedEntity and existingEntity) so only a single well-formed
documentation block remains; keep one <summary> describing synchronization, one
set of <typeparam name="T">, <param name="context">, <param
name="updatedEntity">, <param name="existingEntity">, <returns>, and <exception>
tags, and delete the redundant second <summary> and duplicated <param> entries
to produce valid XML documentation for the method.

In
`@src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs`:
- Around line 369-419: HasMutations currently only checks top-level key equality
and scalar differences (via HasScalarDifferences) so nested navigation changes
can be missed; update HasMutations to detect mutations inside matched entities'
navigations as well by, after finding a matching item
(EntityKeyHelper.FindByKey) and confirming keys equal, enumerating that entity's
NavigationEntries (from context.Entry(match).Navigations or the navigation
metadata) and calling the same mutation check recursively (reusing HasMutations
/ TryGetUpdatedNavigationValue) for each child navigation; do this for both
CollectionEntry item comparisons and the ReferenceEntry path so any
nested/unsupported-branch changes are detected and cause HasMutations to return
true.
- Around line 139-153: The mutation detection currently uses
updatedEntity.GetType().GetProperty(navMetadata.Name) which misses field-backed
navigations; update HasUnloadedMutationAttempt to read the navigation value via
navMetadata.PropertyInfo if not null (use PropertyInfo.GetValue on
updatedEntity) and otherwise use navMetadata.FieldInfo (FieldInfo.GetValue) as a
fallback, then preserve the existing null checks and IEnumerable<object>
collection any() logic; similarly apply the same pattern wherever
GetProperty(navMetadata.Name) is used to ensure both property and field-backed
navigations are handled.
- Around line 244-257: The current branch only handles key-mismatches for
OptionalOneToOne; add a similar check for
NavigationClassification.RequiredOneToOne so we don't attempt to update a
tracked dependent with a different key. Specifically, inside the block where
updatedValue and existingValue are non-null, check if classification ==
NavigationClassification.RequiredOneToOne and !ReferenceKeysMatch(context,
existingValue, updatedValue), and in that case invoke the same detach/replace
flow (e.g., call OptionalOneToOneStrategy.ReplaceDependent(context,
existingNavigation, updatedValue) or a new
RequiredOneToOneStrategy.ReplaceDependent if you prefer) and return, so scalar
updates via RelatedEntityMutationService.UpdateScalarProperties are not
attempted on a mismatched required dependent.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`:
- Around line 12-23: Fix the malformed XML doc block in
OneToOneOwnershipResolver: remove the duplicate <summary> entry and close the
missing </returns> so the doc has a single <summary> describing the method that
determines whether the dependent end of a one-to-one relationship is required
(the method that evaluates the EF Core ForeignKey parameter), a single <param
name="foreignKey"> tag, and a properly closed <returns> tag describing
true/false behavior; ensure the cleaned-up XML sits immediately above the method
that inspects the foreign key metadata.
- Around line 24-28: Remove the dead helper method IsRequiredDependent from
OneToOneOwnershipResolver: delete the public static bool
IsRequiredDependent(IForeignKey foreignKey) method (including its
ArgumentNullException.ThrowIfNull and return foreignKey.IsRequired) since
GraphUpdateOrchestrator.ClassifyNavigation already reads foreignKey.IsRequired
and there are no call sites; if intended for public API compatibility, instead
mark it obsolete with an explanatory message and redirect callers to use
foreignKey.IsRequired.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`:
- Around line 13-20: Fix the malformed XML doc on RequiredOneToOneStrategy by
closing the first <summary> tag and removing the duplicate summary block so the
method-level documentation is a single well-formed <summary> describing that it
marks the dependent referenced by existingNavigation for deletion; ensure the
param tags remain intact for context and parameters (refer to the
RequiredOneToOneStrategy class and the method that accepts DbContext and
existingNavigation) so the XML comments validate correctly.

In `@src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`:
- Around line 63-71: The XML documentation for the method in NavigationLoadGuard
(ValidateNavigationLoaded) contains a typo: an opening <summary> tag is used
twice instead of closing the first; replace the second "<summary>" with a
closing "</summary>" (or remove the duplicate) so the XML doc is well-formed and
the summary reads: "Validates that a specific navigation is loaded before
attempting mutation on it. Throws <see
cref=\"UnloadedNavigationMutationException\"/> if not." Ensure the parameter and
exception XML tags remain unchanged.
- Around line 14-21: The XML doc for NavigationLoadGuard has malformed/unclosed
<summary> tags (duplicate opening tags at the top comment) — consolidate the two
<summary> blocks into a single well-formed <summary> and ensure each opening
<summary> has a matching </summary>, then update the surrounding XML (the <param
name="existingEntry"> and <exception cref="UnloadedNavigationMutationException">
lines) so they remain inside the single summary/remarks block; locate the
comment above the NavigationLoadGuard declaration and fix the tags accordingly.
- Around line 1-3: The file NavigationLoadGuard.cs is missing the System.Linq
namespace causing the call to collectionEntry.CurrentValue.Cast<object>() to not
compile; add the using System.Linq; directive at the top of the file (alongside
existing usings) so the Enumerable.Cast<T>() extension is available for code in
the NavigationLoadGuard class and any methods referencing
collectionEntry.CurrentValue.Cast<object>().

---

Nitpick comments:
In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`:
- Around line 121-125: The IsRetryableSchemaException method currently treats
every InvalidOperationException as retryable which is too broad; narrow it by
removing the blanket InvalidOperationException check and instead only treat
InvalidOperationException as retryable when it matches known transient scenarios
(e.g., specific message/errno/inner-exception patterns caused by SQL
connectivity or pool initialization), or drop it entirely and rely on
SqlException/TimeoutException plus the cancellation/OperationCanceledException
branch; update IsRetryableSchemaException to check exception is SqlException or
TimeoutException or (cancellationSource.IsCancellationRequested && exception is
OperationCanceledException), and if you must keep InvalidOperationException, add
targeted checks for known transient messages/inner-exceptions in that function
(use the exception.Message or exception.InnerException type to identify
transient pool/startup errors).

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedDataStructureTests.cs`:
- Around line 53-57: The test Course1Id_and_Course2Id_share_same_catalog_prefix
currently only asserts
SeedData.Course1Id.Should().NotBe(SeedData.Course2Id);—either rename the test to
reflect that it only verifies inequality, or add an assertion that checks the
catalog prefix invariant: extract the catalog prefix from SeedData.Course1Id and
SeedData.Course2Id (using the same parsing logic used in your ID type) and
assert they are equal while still asserting the IDs are not equal; update the
test name if you choose to keep only the inequality check (e.g.,
CourseIds_are_distinct) or keep the existing name and add the prefix equality
assertion.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`:
- Around line 55-83: The per-item context.Find call in PureManyToManyStrategy
(see usage of context.Find, updatedItem and updatedKeys) can cause N+1 queries
for large updatedItems collections; modify the logic to batch-load candidate
entities before the loop by collecting all primary key values for the target
entity type, querying the DbSet for those keys in a single query to build a
lookup (dictionary by key), then inside the loop use the lookup to resolve
knownEntity and only fall back to context.Find or Add when a key is missing;
keep ApplyValuesIfNotModified and AddToCollection usage but replace the repeated
context.Find calls with the preloaded lookup to avoid many DB roundtrips.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d43ba35b-6820-46dc-8c68-ca2a037a911f

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6c39d and 067ecf5.

📒 Files selected for processing (33)
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseMentorAssignmentConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CoursePolicyConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/LearningCatalogConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorWorkspaceConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/TopicTagConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/TestDbContext.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestBase.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedDataStructureTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/Exceptions/GraphUpdateExceptionTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/RelationshipSemantics/ManyToManyDiffStrategyTests.cs
  • src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnloadedNavigationMutationException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/RelatedEntityMutationService.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs
✅ Files skipped from review due to trivial changes (7)
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorWorkspaceConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/TopicTagConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Diwink.Extensions.EntityFrameworkCore.Tests.Integration.csproj
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/RelatedEntityMutationService.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs
  • src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CoursePolicyConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/LearningCatalogConfiguration.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnloadedNavigationMutationException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs
  • src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestBase.cs
  • src/Diwink.Extensions.EntityFrameworkCore.TestModel/TestDbContext.cs
  • src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs
  • src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs

Comment on lines +18 to +24
/// <summary>
/// Creates a fresh DbContext pointing at the container and ensures the schema exists.
/// <summary>
/// Creates a TestDbContext configured to use SQL Server with a test-specific connection string.
/// </summary>
/// <param name="connectionString">Base SQL Server connection string; if its initial catalog is empty or equals "master", the catalog will be replaced with the test database name.</param>
/// <returns>A new TestDbContext configured with the adjusted connection string.</returns>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed XML documentation - duplicate <summary> tags.

Lines 18-20 contain an incomplete summary block that isn't closed before another <summary> starts on line 21. This will generate compiler warning CS1570.

📝 Proposed fix
-    /// <summary>
-    /// Creates a fresh DbContext pointing at the container and ensures the schema exists.
-    /// <summary>
     /// <summary>
     /// Creates a TestDbContext configured to use SQL Server with a test-specific connection string.
     /// </summary>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>
/// Creates a fresh DbContext pointing at the container and ensures the schema exists.
/// <summary>
/// Creates a TestDbContext configured to use SQL Server with a test-specific connection string.
/// </summary>
/// <param name="connectionString">Base SQL Server connection string; if its initial catalog is empty or equals "master", the catalog will be replaced with the test database name.</param>
/// <returns>A new TestDbContext configured with the adjusted connection string.</returns>
/// <summary>
/// Creates a TestDbContext configured to use SQL Server with a test-specific connection string.
/// </summary>
/// <param name="connectionString">Base SQL Server connection string; if its initial catalog is empty or equals "master", the catalog will be replaced with the test database name.</param>
/// <returns>A new TestDbContext configured with the adjusted connection string.</returns>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
around lines 18 - 24, The XML doc for the method in DatabaseBootstrap contains
duplicate/malformed <summary> tags; open the documentation for the method that
"Creates a TestDbContext configured to use SQL Server..." (the method in class
DatabaseBootstrap that returns a TestDbContext and accepts a connectionString)
and merge the two <summary> blocks into a single well-formed
<summary>...</summary> describing the behavior, then ensure the existing <param>
and <returns> tags remain intact and properly closed so the XML doc compiles
without CS1570.

Comment on lines +53 to +58
/// <summary>
/// Ensures the database schema is created. Called once per test collection.
/// <summary>
/// Ensures the test database schema exists for the provided SQL Server connection string.
/// </summary>
/// <param name="connectionString">SQL Server connection string. If the Initial Catalog is empty or "master", the configured test database name will be used.</param>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed XML documentation - same pattern as above.

Lines 53-55 have the same unclosed <summary> issue.

📝 Proposed fix
-    /// <summary>
-    /// Ensures the database schema is created. Called once per test collection.
-    /// <summary>
     /// <summary>
     /// Ensures the test database schema exists for the provided SQL Server connection string.
     /// </summary>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
around lines 53 - 58, The XML documentation for the DatabaseBootstrap method
contains a duplicated and unclosed <summary> tag (the first <summary> is not
closed), causing malformed XML comments; update the doc on the
DatabaseBootstrap/EnsureDatabaseSchemaCreated member so there is a single
well-formed <summary>...</summary> block (remove the extra <summary> line or
close it properly) and ensure the param description remains inside a proper
<param name="connectionString">...</param> tag.

Comment on lines +67 to +73
/// <summary>
/// Drops and recreates the database schema. Used for test isolation
/// when a test needs a guaranteed clean slate.
/// <summary>
/// Deletes and recreates the test database schema for the given connection string.
/// </summary>
/// <param name="connectionString">The database connection string used to locate the server; if the connection string has no catalog or specifies "master", the configured test database name will be used.</param>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed XML documentation - same pattern.

Lines 67-70 have the same unclosed <summary> issue.

📝 Proposed fix
-    /// <summary>
-    /// Drops and recreates the database schema. Used for test isolation
-    /// when a test needs a guaranteed clean slate.
-    /// <summary>
     /// <summary>
     /// Deletes and recreates the test database schema for the given connection string.
     /// </summary>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>
/// Drops and recreates the database schema. Used for test isolation
/// when a test needs a guaranteed clean slate.
/// <summary>
/// Deletes and recreates the test database schema for the given connection string.
/// </summary>
/// <param name="connectionString">The database connection string used to locate the server; if the connection string has no catalog or specifies "master", the configured test database name will be used.</param>
/// <summary>
/// Deletes and recreates the test database schema for the given connection string.
/// </summary>
/// <param name="connectionString">The database connection string used to locate the server; if the connection string has no catalog or specifies "master", the configured test database name will be used.</param>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
around lines 67 - 73, The XML doc for the database-reset method contains a
duplicated and unclosed <summary> tag; fix the XML by consolidating the two
summary lines into a single well-formed XML comment (one <summary> ...
</summary>) for the method in DatabaseBootstrap (the method that "Deletes and
recreates the test database schema for the given connection string"), ensuring
the opening <summary> is closed and no duplicate <summary> tag remains so XML
documentation parses correctly.

Comment on lines +23 to +28
/// <summary>
/// Seeds a complete test scenario with all relationship types represented.
/// <summary>
/// Seeds the provided test database with a complete, deterministic integration scenario: three topic tags, two mentors (one with a workspace), a learning catalog containing two courses with policies, tag associations, and a mentor assignment for the first course, then saves the changes to the context.
/// </summary>
/// <param name="context">The TestDbContext to which seeded entities are added and persisted.</param>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed XML documentation: duplicate <summary> tag.

The XML documentation has two <summary> opening tags without properly closing the first one. This will cause XML doc generation warnings.

📝 Proposed fix
     /// <summary>
     /// Seeds a complete test scenario with all relationship types represented.
-    /// <summary>
-    /// Seeds the provided test database with a complete, deterministic integration scenario: three topic tags, two mentors (one with a workspace), a learning catalog containing two courses with policies, tag associations, and a mentor assignment for the first course, then saves the changes to the context.
     /// </summary>
+    /// <remarks>
+    /// Seeds the provided test database with a complete, deterministic integration scenario: three topic tags, two mentors (one with a workspace), a learning catalog containing two courses with policies, tag associations, and a mentor assignment for the first course, then saves the changes to the context.
+    /// </remarks>
     /// <param name="context">The TestDbContext to which seeded entities are added and persisted.</param>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs`
around lines 23 - 28, The XML doc for the seeding method in the SeedData class
is malformed due to a duplicate opening <summary> tag; fix it by consolidating
the two <summary> entries into a single well-formed XML summary block (one
opening <summary>, the combined description, then a single closing </summary>),
and ensure the <param name="context"> tag remains outside that summary and
correctly formed; update the comment above the seeding method in SeedData.cs so
XML doc generation warnings are resolved.

Comment on lines +223 to +229
public void UnsupportedNavigationMutatedException_with_empty_strings_stores_them_as_is()
{
var ex = new UnsupportedNavigationMutatedException(string.Empty, string.Empty);

ex.RelationshipPath.Should().Be(string.Empty);
ex.RelationshipType.Should().Be(string.Empty);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix failing empty-string test expectation (Line 223).

This test contradicts the current GraphUpdateException validation contract and is already failing CI. It should assert rejection of empty relationshipPath, not successful storage.

Proposed test fix
 [Fact]
-public void UnsupportedNavigationMutatedException_with_empty_strings_stores_them_as_is()
+public void UnsupportedNavigationMutatedException_rejects_empty_relationship_path()
 {
-    var ex = new UnsupportedNavigationMutatedException(string.Empty, string.Empty);
-
-    ex.RelationshipPath.Should().Be(string.Empty);
-    ex.RelationshipType.Should().Be(string.Empty);
+    var act = () => new UnsupportedNavigationMutatedException(string.Empty, string.Empty);
+
+    act.Should().Throw<ArgumentException>()
+        .Which.ParamName.Should().Be("relationshipPath");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void UnsupportedNavigationMutatedException_with_empty_strings_stores_them_as_is()
{
var ex = new UnsupportedNavigationMutatedException(string.Empty, string.Empty);
ex.RelationshipPath.Should().Be(string.Empty);
ex.RelationshipType.Should().Be(string.Empty);
}
[Fact]
public void UnsupportedNavigationMutatedException_rejects_empty_relationship_path()
{
var act = () => new UnsupportedNavigationMutatedException(string.Empty, string.Empty);
act.Should().Throw<ArgumentException>()
.Which.ParamName.Should().Be("relationshipPath");
}
🧰 Tools
🪛 GitHub Actions: .NET CI

[error] 225-225: xUnit test failed: Diwink.Extensions.EntityFrameworkCore.Tests.Unit.Exceptions.GraphUpdateExceptionTests.UnsupportedNavigationMutatedException_with_empty_strings_stores_them_as_is. Error: System.ArgumentException: Relationship path cannot be empty or whitespace. (Parameter 'relationshipPath')

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore.Tests.Unit/Exceptions/GraphUpdateExceptionTests.cs`
around lines 223 - 229, The test currently asserts that
UnsupportedNavigationMutatedException(string.Empty, string.Empty) stores empty
strings, but per GraphUpdateException validation it should reject an empty
relationshipPath; update the test to assert that constructing
UnsupportedNavigationMutatedException with an empty relationshipPath throws the
appropriate validation exception (e.g., ArgumentException or the specific
validation exception used by GraphUpdateException) instead of expecting
successful storage; reference the UnsupportedNavigationMutatedException
constructor in the assertion.

Comment on lines +24 to +28
public static bool IsRequiredDependent(IForeignKey foreignKey)
{
ArgumentNullException.ThrowIfNull(foreignKey);
return foreignKey.IsRequired;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if IsRequiredDependent is actually called anywhere in the codebase

rg -n 'IsRequiredDependent' --type=cs

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 245


Remove unused IsRequiredDependent helper method.

The IsRequiredDependent method is not called anywhere in the codebase. Code search confirms only the definition exists with zero call sites, while GraphUpdateOrchestrator.ClassifyNavigation directly accesses foreignKey.IsRequired instead. Remove this dead code unless it's intentionally preserved for public API compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`
around lines 24 - 28, Remove the dead helper method IsRequiredDependent from
OneToOneOwnershipResolver: delete the public static bool
IsRequiredDependent(IForeignKey foreignKey) method (including its
ArgumentNullException.ThrowIfNull and return foreignKey.IsRequired) since
GraphUpdateOrchestrator.ClassifyNavigation already reads foreignKey.IsRequired
and there are no call sites; if intended for public API compatibility, instead
mark it obsolete with an explanatory message and redirect callers to use
foreignKey.IsRequired.

Comment on lines +13 to +20
/// <summary>
/// Removes the required dependent by marking it for deletion.
/// EF Core will delete the row on SaveChanges.
/// <summary>
/// Marks the dependent entity referenced by <paramref name="existingNavigation"/> for deletion if it is currently tracked.
/// </summary>
/// <param name="context">The <see cref="DbContext"/> used to mark the dependent entity for removal.</param>
/// <param name="existingNavigation">The tracked reference entry whose <c>CurrentValue</c> is the dependent entity to remove; no action is taken if its value is <c>null</c>.</param>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed XML documentation: unclosed </summary> and duplicate tags.

Same pattern as other files - Line 16 starts <summary> without closing the previous block at Line 15.

📝 Proposed fix
     /// <summary>
     /// Removes the required dependent by marking it for deletion.
     /// EF Core will delete the row on SaveChanges.
-    /// <summary>
-    /// Marks the dependent entity referenced by <paramref name="existingNavigation"/> for deletion if it is currently tracked.
     /// </summary>
     /// <param name="context">The <see cref="DbContext"/> used to mark the dependent entity for removal.</param>
-    /// <param name="existingNavigation">The tracked reference entry whose <c>CurrentValue</c> is the dependent entity to remove; no action is taken if its value is <c>null</c>.</param>
+    /// <param name="existingNavigation">The tracked reference entry whose <c>CurrentValue</c> is the dependent entity to remove; no action is taken if <c>CurrentValue</c> is <c>null</c>.</param>
     public static void RemoveDependent(DbContext context, ReferenceEntry existingNavigation)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`
around lines 13 - 20, Fix the malformed XML doc on RequiredOneToOneStrategy by
closing the first <summary> tag and removing the duplicate summary block so the
method-level documentation is a single well-formed <summary> describing that it
marks the dependent referenced by existingNavigation for deletion; ensure the
param tags remain intact for context and parameters (refer to the
RequiredOneToOneStrategy class and the method that accepts DbContext and
existingNavigation) so the XML comments validate correctly.

Comment on lines +1 to +3
using Diwink.Extensions.EntityFrameworkCore.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if System.Linq is already imported elsewhere or if this file compiles
# Check the current imports in the file
cat src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs | head -20

# Check if there's a global using for System.Linq in the project
fd -e cs -p 'GlobalUsings|Usings' --exec cat {} \; 2>/dev/null || echo "No global usings file found"
rg -l 'global using.*System\.Linq' --type cs

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 1056


🏁 Script executed:

# Check line 49 and surrounding context
sed -n '45,55p' src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 656


🏁 Script executed:

# Look for global using statements anywhere in the project
find . -name "*.cs" -type f | xargs grep -l "global using" 2>/dev/null | head -5

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 64


🏁 Script executed:

# Verify the complete list of usings at the top of the file
head -10 src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs

Repository: WahidBitar/EF-Core-Simple-Graph-Update

Length of output: 514


Add missing using System.Linq; directive to resolve compilation error.

Line 49 calls .Cast<object>() on collectionEntry.CurrentValue, which is an extension method from System.Linq.Enumerable. This import is required for the code to compile.

Proposed fix
 using Diwink.Extensions.EntityFrameworkCore.Exceptions;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.ChangeTracking;
+using System.Linq;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using Diwink.Extensions.EntityFrameworkCore.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Diwink.Extensions.EntityFrameworkCore.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Linq;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`
around lines 1 - 3, The file NavigationLoadGuard.cs is missing the System.Linq
namespace causing the call to collectionEntry.CurrentValue.Cast<object>() to not
compile; add the using System.Linq; directive at the top of the file (alongside
existing usings) so the Enumerable.Cast<T>() extension is available for code in
the NavigationLoadGuard class and any methods referencing
collectionEntry.CurrentValue.Cast<object>().

Comment on lines +14 to +21
/// <summary>
/// Ensures all navigations in the existing entity entry that the caller expects
/// to participate in graph mutation are explicitly loaded.
/// <summary>
/// Validates loaded navigations on the tracked entity and its reachable object graph, ensuring required navigations are present before mutation.
/// </summary>
/// <param name="existingEntry">The tracked entity entry whose navigations and reachable entities will be validated.</param>
/// <exception cref="UnloadedNavigationMutationException">Thrown when a required navigation encountered during validation is not loaded.</exception>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

XML documentation has unclosed <summary> tags.

Line 17 and line 66 both have <summary> where </summary> is expected. This creates malformed XML documentation.

📝 Proposed fix for line 17
     /// <summary>
     /// Ensures all navigations in the existing entity entry that the caller expects
     /// to participate in graph mutation are explicitly loaded.
-    /// <summary>
+    /// </summary>
     /// <summary>
     /// Validates loaded navigations on the tracked entity and its reachable object graph, ensuring required navigations are present before mutation.
     /// </summary>

Note: There appear to be two summary blocks here. Consider consolidating into a single <summary> block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`
around lines 14 - 21, The XML doc for NavigationLoadGuard has malformed/unclosed
<summary> tags (duplicate opening tags at the top comment) — consolidate the two
<summary> blocks into a single well-formed <summary> and ensure each opening
<summary> has a matching </summary>, then update the surrounding XML (the <param
name="existingEntry"> and <exception cref="UnloadedNavigationMutationException">
lines) so they remain inside the single summary/remarks block; locate the
comment above the NavigationLoadGuard declaration and fix the tags accordingly.

Comment on lines +63 to +71
/// <summary>
/// Validates that a specific navigation is loaded before attempting mutation on it.
/// Throws <see cref="UnloadedNavigationMutationException"/> if not.
/// <summary>
/// Ensures the specified navigation is loaded before performing a mutation.
/// </summary>
/// <param name="navigation">The navigation entry to validate is loaded.</param>
/// <param name="entityPath">A slash-delimited path that identifies the entity location used in the exception message.</param>
/// <exception cref="UnloadedNavigationMutationException">Thrown when <paramref name="navigation"/> is not loaded.</exception>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same XML documentation typo at line 66.

📝 Proposed fix
     /// <summary>
     /// Validates that a specific navigation is loaded before attempting mutation on it.
     /// Throws <see cref="UnloadedNavigationMutationException"/> if not.
-    /// <summary>
+    /// </summary>
+    /// <remarks>
     /// Ensures the specified navigation is loaded before performing a mutation.
-    /// </summary>
+    /// </remarks>
     /// <param name="navigation">The navigation entry to validate is loaded.</param>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`
around lines 63 - 71, The XML documentation for the method in
NavigationLoadGuard (ValidateNavigationLoaded) contains a typo: an opening
<summary> tag is used twice instead of closing the first; replace the second
"<summary>" with a closing "</summary>" (or remove the duplicate) so the XML doc
is well-formed and the summary reads: "Validates that a specific navigation is
loaded before attempting mutation on it. Throws <see
cref=\"UnloadedNavigationMutationException\"/> if not." Ensure the parameter and
exception XML tags remain unchanged.

@WahidBitar WahidBitar closed this Apr 5, 2026
WahidBitar added a commit that referenced this pull request Apr 5, 2026
…cs v2 rebuild: EF Core 10 targeting with modular refactoring @coderabbitai (#16)

* refactored everything O_o
* huge renaming
* build(ci): update github actions and fix yaml formatting

Update actions/checkout from v2 to v4, actions/setup-dotnet from v1 to v4, and change dotnet-version to "10.0.x". Also fix indentation and remove extra spaces in branch names.

* ci: enhance workflow with publishing and version bump

Add a new publish job to the CI pipeline that builds, packs, and publishes the NuGet package on master pushes, with conditional checks for API key availability. Update action versions (checkout@v6, setup-dotnet@v5), adjust commands to target the solution file, and refine job names and permissions.

BREAKING CHANGE: package version bumped from 2.0.0 to 10.0.0 to align with EF Core 10+ targeting, which may introduce incompatibilities.

* Rebuild graph update system with contract-driven relationship semantics; targeting EF Core 10

* 📝 Add docstrings to `001-rebuild-graph-update`

Docstrings generation was requested by @WahidBitar.

* #13 (comment)

The following files were modified:

* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CourseMentorAssignmentConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/CoursePolicyConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/LearningCatalogConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/MentorWorkspaceConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/Configurations/TopicTagConfiguration.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.TestModel/TestDbContext.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/DatabaseBootstrap.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/IntegrationTestBase.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SeedData.cs`
* `src/Diwink.Extensions.EntityFrameworkCore.Tests.Integration/Infrastructure/SqlServerContainerFixture.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/AmbiguousOwnershipSemanticsException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/GraphUpdateException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/PartialMutationNotAllowedException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnloadedNavigationMutationException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedNavigationMutatedException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Exceptions/UnsupportedRelationshipPatternException.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/EntityKeyHelper.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/GraphUpdateOrchestrator.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/OperationGuard.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/GraphUpdate/RelatedEntityMutationService.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OneToOneOwnershipResolver.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/OptionalOneToOneStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PayloadManyToManyStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/PureManyToManyStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/RelationshipStrategies/RequiredOneToOneStrategy.cs`
* `src/Diwink.Extensions.EntityFrameworkCore/Traversal/NavigationLoadGuard.cs`

* CodeRabbit Generated Unit Tests: Add unit tests for PR changes

* Harden graph update helpers and test infrastructure

* Preserve modified tracked many-to-many entities

* Rename graph update API and remove mutation service

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant